﻿/*
   Created 2009 March 11 Randall Maas
   Copyright (c) 2009, Randall Maas
   All rights reserved.

   Redistribution and use in source and binary forms, with or without
   modification, are permitted provided that the following conditions are met:

   Redistributions of source code must retain the above copyright notice, this
   list of conditions and the following disclaimer.

   Redistributions in binary form must reproduce the above copyright notice,
   this list of conditions and the following disclaimer in the documentation
   and/or other materials provided with the distribution.

   Neither the name of Blackwood Designs, LLC nor the names of its contributors
   may be used to endorse or promote products derived from this software without
   specific prior written permission.

   THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
   AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
   IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
   ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
   LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
   CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
   SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
   INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
   CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
   ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
   THE POSSIBILITY OF SUCH DAMAGE.
 */

using System;
using System . Text;

/// <summary>
/// Automatic gain control class.  It resamples to 16Khz
/// </summary>
internal partial class GainControl
{
   #region Control Parameters
   /// <summary>
   /// The minimum dB before we cut to silence
   /// </summary>
   internal static double CutOff_dB;

   /// <summary>
   /// When talking below this level, we amplify it to about this level
   /// </summary>
   internal static double LowGain_dB;

   /// <summary>
   /// The typical target gain level we are targetting
   /// </summary>
   internal static double TgtGain_dB;
   #endregion

   #region Internal Flags
   const double db2log = 0.11512925464970228420089957273422; // ln(10) / 20 

   /// <summary>
   /// Amount to compress frequency by.  Typically 0 .. 0.03
   /// </summary>
   const double FreqReduction = 0.03;

   /// <summary>
   /// Set to true if it should attempt to adjust the gain, false if it should not
   /// </summary>
   internal bool AutoGain=true;

   /// <summary>
   /// Used when down sampling to 16000/per second.  This is the number of
   /// input samples times 1024
   /// </summary>
   /// <remarks>
   /// Scaled by sample rate
   /// </remarks>
   int InInc;

   // Counts of what we squelched
   internal static int Squelched = 0;
   internal static double LowCount = 0.0;
   internal static double HiCount  = 0.0;

   // If the sample rate isn't 16000/sec we will have to low pass filter and downsample
   internal bool DoLP = false;

   /// <summary>
   /// Low pass filter
   /// </summary>
   double[] LP;

   /// <summary>
   /// The gain setting used for the last sample window
   /// </summary>
   double PrevGain = 0.0;

   /// <summary>
   /// The low-pass filter state variables
   /// </summary>
   double DS_O_1=0.0, DS_O_2=0.0,DS_O_3=0.0,DS_O_4=0.0, DS_I_1=0.0, DS_I_2=0.0,DS_I_3=0.0,DS_I_4=0.0;
   #endregion

   /// <summary>
   /// Constructor for the gain control
   /// </summary>
   /// <param name="SampleRate">The number of samples per second (48000, 44100, or 16000)</param>
   /// <param name="NumSamplesPerWindow">The number of samples</param>
   internal GainControl(int SampleRate, int NumSamplesPerWindow)
   {
      // Calculate how we resample to 16Khz
      InInc = (int) (1024.0*SampleRate * (1.0 - FreqReduction) / 16000.0);

      // If the sample rate isn't 16000/sec we will have to low pass filter and downsample
      DoLP = true;

      // Look up the coefficients based on the sample rate
      switch (SampleRate)
      {
         case 48000: YuleCoefs = EqLoudness_48k; HPCoefs = HP_150_48k; LP = LP_8k_48k; break;
         case 44100: YuleCoefs = EqLoudness_44k; HPCoefs = HP_150_44k; LP = LP_8k_44k; break;
         case 16000: YuleCoefs = EqLoudness_16k; HPCoefs = HP_150_16k; DoLP = false; break;
      }

      YuleTmp = new double[10+NumSamplesPerWindow];
      double QSize = dBWindow/((double) NumSamplesPerWindow/SampleRate)+1;
      MSQueue = new double[(int) QSize];
      SortedQ = new double[(int) QSize];
      Q95Idx  = (int)(0.95*QSize);
   }

   /// <summary>
   /// This is passed a buffer of samples, on which it removes clicks, performs
   /// autogain, squelches noise and downsamples the results to 16000hz, as
   /// appropriate
   /// </summary>
   /// <param name="InBuffer">The buffer</param>
   /// <param name="Ofs">The start of the samples in the buffer (before this are old samples)</param>
   /// <param name="OutBuffer">The gained', filtered, resampled and encoded samples</param>
   /// <returns>The number of bytes in the buffer that hold 16000 samples</returns>
   internal int Process(Int16[] InBuffer, int Ofs, byte[] OutBuffer)
   {
      // Convert the sample buffer (!)
      int NumSamples = InBuffer.Length;


      // --- Adjust the gain of the sound -------------------------------------
      #region Analyze dB for Gain
      // Feed this into the analysis of dB
      double Gain;
      if (!AutoGain)
        Gain=1.0;
       else
        {
           double MaxGain;
           double  dB = Analyze(InBuffer, Ofs, out MaxGain);

           if (dB < CutOff_dB )
             Gain = 0.0;
           else if (dB < LowGain_dB + 4.0 )
             {
                Gain = Math.Exp((LowGain_dB-dB)*db2log);
                LowCount += Gain-1.0;
             }
           else
             {
                Gain = Math.Exp((TgtGain_dB-dB)*db2log);
                HiCount += Gain-1.0;
             }
           Gain = (0.4*Gain + 0.6*PrevGain);
           if (Gain > MaxGain)
             Gain = MaxGain;
           PrevGain = Gain;
        }
      #endregion

      // Skip further process if there is silence
      if (0.0 == Gain)
        {
           Squelched ++;
           return 0;
        }

      bool ReduceVolume = Gain != 0.0 && Gain < 1.0;

      int End = OutBuffer.Length;
      int NextIdxForOut = -InInc;
      int OutIdx = 0;
      for (int I = Ofs; OutIdx < End && I < NumSamples; I++)
      {
         // Retrieve sample
         Int16 S = InBuffer[I];
         double I_0 =S;

         // Apply Gain
         I_0 *= Gain;

         #region 8khz low pass filter 
         if (DoLP)
           {
              // Simple Butterworth 8KHz low-pass filter
              double A = (I_0+DS_I_2) * LP[0] + DS_I_1 * LP[1];
              DS_I_2 = DS_I_1;
              DS_I_1 = I_0;
              I_0 = A - DS_O_1 * LP[2] - DS_O_2 * LP[3];
              DS_O_2 = DS_O_1;
              DS_O_1 = I_0;

              #if false  // 150Hz to 8Khz bandpass -- sounds a little tinny
              double A = (I_0+DS_I_4) * 0.54468383601666 + DS_I_2*-1.08936767203333;

              DS_I_4 = DS_I_3;
              DS_I_3 = DS_I_2;
              DS_I_2 = DS_I_1;
              DS_I_1 = I_0;
              I_0 = A - DS_O_1 * -0.87382518179648 - DS_O_2 * -0.60768897646037
                      - DS_O_3 * 0.19534815286292  - DS_O_4 *  0.30870802929921;
              DS_O_4 = DS_O_3;
              DS_O_3 = DS_O_2;
              DS_O_2 = DS_O_1;
              DS_O_1 = I_0;
              #endif

              #region Change sample rate
              int Tmp = NextIdxForOut / 1024;
              if (I < Tmp)
                continue;
              NextIdxForOut += InInc;
              #endregion 
           }
         #endregion


         #region Region convert to 16bit

         // Convert it back to 16 bit
         S = (Int16) I_0;

         // Store it
         OutBuffer[OutIdx++] = (byte)(S & 0xFF);
         OutBuffer[OutIdx++] = (byte)(((UInt16)S >> 8) & 0xFF);
         #endregion

      }
      return OutIdx;
   }
}
